/*
 * Created on Mar 22, 2013
 * Created by Paul Gardner
 * 
 * Copyright 2013 Azureus Software, Inc.  All rights reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package com.aelitis.azureus.core.tag.impl;

import java.util.List;
import java.util.Map;
import java.util.Set;

import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.download.DownloadManagerStats;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.networkmanager.LimitedRateGroup;
import com.aelitis.azureus.core.tag.Tag;
import com.aelitis.azureus.core.tag.TagDownload;
import com.aelitis.azureus.core.tag.TagFeature;
import com.aelitis.azureus.core.tag.TagFeatureProperties;
import com.aelitis.azureus.core.tag.TagFeatureRateLimit;
import com.aelitis.azureus.core.tag.TagFeatureRunState;
import com.aelitis.azureus.core.tag.TagListener;
import com.aelitis.azureus.core.tag.Taggable;

public class TagDownloadWithState extends TagWithState implements TagDownload {
    private int upload_rate_limit;
    private int download_rate_limit;

    private int upload_rate = -1;
    private int download_rate = -1;

    private long last_rate_update;

    private Object UPLOAD_PRIORITY_ADDED_KEY = new Object();
    private int upload_priority;
    private int min_share_ratio;
    private int max_share_ratio;

    private boolean supports_xcode;
    private boolean supports_file_location;

    private LimitedRateGroup upload_limiter = new LimitedRateGroup() {
        public String getName() {
            return ("tag_up: " + getTagName(true));
        }

        public int getRateLimitBytesPerSecond() {
            return (upload_rate_limit);
        }

        public void updateBytesUsed(int used) {

        }
    };

    private LimitedRateGroup download_limiter = new LimitedRateGroup() {
        public String getName() {
            return ("tag_down: " + getTagName(true));
        }

        public int getRateLimitBytesPerSecond() {
            return (download_rate_limit);
        }

        public void updateBytesUsed(int used) {

        }
    };

    private boolean do_rates;
    private boolean do_up;
    private boolean do_down;

    private int run_states;

    private static AsyncDispatcher rs_async = new AsyncDispatcher(2000);

    private TagProperty[] tag_properties = new TagProperty[] {
            createTagProperty(TagFeatureProperties.PR_TRACKERS, TagFeatureProperties.PT_STRING_LIST),
            createTagProperty(TagFeatureProperties.PR_UNTAGGED, TagFeatureProperties.PT_BOOLEAN),
            createTagProperty(TagFeatureProperties.PR_TRACKER_TEMPLATES, TagFeatureProperties.PT_STRING_LIST),
            createTagProperty(TagFeatureProperties.PR_CONSTRAINT, TagFeatureProperties.PT_STRING_LIST) };

    public TagDownloadWithState(TagTypeBase tt, int tag_id, String name, boolean do_rates, boolean do_up, boolean do_down, int run_states) {
        super(tt, tag_id, name);

        init(do_rates, do_up, do_down, run_states);
    }

    protected TagDownloadWithState(TagTypeBase tt, int tag_id, Map details, boolean do_rates, boolean do_up, boolean do_down, int run_states) {
        super(tt, tag_id, details);

        init(do_rates, do_up, do_down, run_states);
    }

    private void init(boolean _do_rates, boolean _do_up, boolean _do_down, int _run_states) {
        do_rates = _do_rates;
        do_up = _do_up;
        do_down = _do_down;
        run_states = _run_states;

        if (do_up) {

            upload_rate_limit = (int) readLongAttribute(AT_RATELIMIT_UP, 0);
        }

        if (do_down) {

            download_rate_limit = (int) readLongAttribute(AT_RATELIMIT_DOWN, 0);
        }

        upload_priority = (int) readLongAttribute(AT_RATELIMIT_UP_PRI, 0);
        min_share_ratio = (int) readLongAttribute(AT_RATELIMIT_MIN_SR, 0);
        max_share_ratio = (int) readLongAttribute(AT_RATELIMIT_MAX_SR, 0);

        addTagListener(new TagListener() {
            public void taggableAdded(Tag tag, Taggable tagged) {
                DownloadManager manager = (DownloadManager) tagged;

                manager.addRateLimiter(upload_limiter, true);
                manager.addRateLimiter(download_limiter, false);

                if (upload_priority > 0) {

                    manager.updateAutoUploadPriority(UPLOAD_PRIORITY_ADDED_KEY, true);
                }

                if (min_share_ratio > 0) {

                    updateMinShareRatio(manager, min_share_ratio);
                }

                if (max_share_ratio > 0) {

                    updateMaxShareRatio(manager, max_share_ratio);
                }
            }

            public void taggableSync(Tag tag) {
            }

            public void taggableRemoved(Tag tag, Taggable tagged) {
                DownloadManager manager = (DownloadManager) tagged;

                manager.removeRateLimiter(upload_limiter, true);
                manager.removeRateLimiter(download_limiter, false);

                if (upload_priority > 0) {

                    manager.updateAutoUploadPriority(UPLOAD_PRIORITY_ADDED_KEY, false);
                }

                if (min_share_ratio > 0) {

                    updateMinShareRatio(manager, 0);
                }

                if (max_share_ratio > 0) {

                    updateMaxShareRatio(manager, 0);
                }
            }

            private void updateMinShareRatio(DownloadManager manager, int sr) {
                List<Tag> dm_tags = getTagType().getTagsForTaggable(manager);

                for (Tag t : dm_tags) {

                    if (t == TagDownloadWithState.this) {

                        continue;
                    }

                    if (t instanceof TagFeatureRateLimit) {

                        int o_sr = ((TagFeatureRateLimit) t).getTagMinShareRatio();

                        if (o_sr > sr) {

                            sr = o_sr;
                        }
                    }
                }

                manager.getDownloadState().setIntParameter(DownloadManagerState.PARAM_MIN_SHARE_RATIO, sr);
            }

            private void updateMaxShareRatio(DownloadManager manager, int sr) {
                List<Tag> dm_tags = getTagType().getTagsForTaggable(manager);

                for (Tag t : dm_tags) {

                    if (t == TagDownloadWithState.this) {

                        continue;
                    }

                    if (t instanceof TagFeatureRateLimit) {

                        int o_sr = ((TagFeatureRateLimit) t).getTagMaxShareRatio();

                        if (o_sr > sr) {

                            sr = o_sr;
                        }
                    }
                }

                manager.getDownloadState().setIntParameter(DownloadManagerState.PARAM_MAX_SHARE_RATIO, sr);
            }
        }, true);
    }

    @Override
    public void removeTag() {
        for (DownloadManager dm : getTaggedDownloads()) {

            dm.removeRateLimiter(upload_limiter, true);
            dm.removeRateLimiter(download_limiter, false);

            if (upload_priority > 0) {

                dm.updateAutoUploadPriority(UPLOAD_PRIORITY_ADDED_KEY, false);
            }
        }

        super.removeTag();
    }

    @Override
    public void addTaggable(Taggable t) {
        if (t instanceof DownloadManager) {

            DownloadManager dm = (DownloadManager) t;

            if (dm.isDestroyed()) {

                // There's a race condition when stopping and removing a torrent that isn't easy to avoid in terms
                // of a download being added to the 'stopped' tag and concurrently removed.
                // There will be a subseqent 'downloadRemoved' action triggered that should tidy up any
                // inconsistency left due to this

                // Debug.out( "Invalid Taggable added - download is destroyed: " + dm.getDisplayName());

            } else {

                super.addTaggable(t);
            }
        } else {

            Debug.out("Invalid Taggable added: " + t);
        }
    }

    public int getTaggableTypes() {
        return (Taggable.TT_DOWNLOAD);
    }

    public Set<DownloadManager> getTaggedDownloads() {
        return ((Set<DownloadManager>) (Object) getTagged());
    }

    public boolean supportsTagRates() {
        return (do_rates);
    }

    public boolean supportsTagUploadLimit() {
        return (do_up);
    }

    public boolean supportsTagDownloadLimit() {
        return (do_down);
    }

    public int getTagUploadLimit() {
        return (upload_rate_limit);
    }

    public void setTagUploadLimit(int bps) {
        if (upload_rate_limit == bps) {

            return;
        }

        if (!do_up) {

            Debug.out("Not supported");

            return;
        }

        upload_rate_limit = bps;

        writeLongAttribute(AT_RATELIMIT_UP, upload_rate_limit);
    }

    public int getTagCurrentUploadRate() {
        updateRates();

        return (upload_rate);
    }

    public int getTagDownloadLimit() {
        return (download_rate_limit);
    }

    public void setTagDownloadLimit(int bps) {
        if (download_rate_limit == bps) {

            return;
        }

        if (!do_down) {

            Debug.out("Not supported");

            return;
        }

        download_rate_limit = bps;

        writeLongAttribute(AT_RATELIMIT_DOWN, download_rate_limit);
    }

    public int getTagCurrentDownloadRate() {
        updateRates();

        return (download_rate);
    }

    public int getTagUploadPriority() {
        return (upload_priority);
    }

    public void setTagUploadPriority(int priority) {
        if (priority < 0) {

            priority = 0;
        }

        if (priority == upload_priority) {

            return;
        }

        int old_up = upload_priority;

        upload_priority = priority;

        writeLongAttribute(AT_RATELIMIT_UP_PRI, priority);

        if (old_up == 0 || priority == 0) {

            Set<DownloadManager> dms = getTaggedDownloads();

            for (DownloadManager dm : dms) {

                dm.updateAutoUploadPriority(UPLOAD_PRIORITY_ADDED_KEY, priority > 0);
            }
        }
    }

    public int getTagMinShareRatio() {
        return (min_share_ratio);
    }

    public void setTagMinShareRatio(int sr) {
        if (sr < 0) {

            sr = 0;
        }

        if (sr == min_share_ratio) {

            return;
        }

        min_share_ratio = sr;

        writeLongAttribute(AT_RATELIMIT_MIN_SR, sr);

        Set<DownloadManager> dms = getTaggedDownloads();

        for (DownloadManager dm : dms) {

            List<Tag> dm_tags = getTagType().getTagsForTaggable(dm);

            for (Tag t : dm_tags) {

                if (t == this) {

                    continue;
                }

                if (t instanceof TagFeatureRateLimit) {

                    int o_sr = ((TagFeatureRateLimit) t).getTagMinShareRatio();

                    if (o_sr > sr) {

                        sr = o_sr;
                    }
                }
            }

            dm.getDownloadState().setIntParameter(DownloadManagerState.PARAM_MIN_SHARE_RATIO, sr);
        }
    }

    public int getTagMaxShareRatio() {
        return (max_share_ratio);
    }

    public void setTagMaxShareRatio(int sr) {
        if (sr < 0) {

            sr = 0;
        }

        if (sr == max_share_ratio) {

            return;
        }

        max_share_ratio = sr;

        writeLongAttribute(AT_RATELIMIT_MAX_SR, sr);

        Set<DownloadManager> dms = getTaggedDownloads();

        for (DownloadManager dm : dms) {

            List<Tag> dm_tags = getTagType().getTagsForTaggable(dm);

            for (Tag t : dm_tags) {

                if (t == this) {

                    continue;
                }

                if (t instanceof TagFeatureRateLimit) {

                    int o_sr = ((TagFeatureRateLimit) t).getTagMaxShareRatio();

                    if (o_sr > sr) {

                        sr = o_sr;
                    }
                }
            }

            dm.getDownloadState().setIntParameter(DownloadManagerState.PARAM_MAX_SHARE_RATIO, sr);
        }
    }

    private void updateRates() {
        long now = SystemTime.getCurrentTime();

        if (now - last_rate_update > 2500) {

            int new_up = 0;
            int new_down = 0;

            Set<DownloadManager> dms = getTaggedDownloads();

            if (dms.size() == 0) {

                new_up = -1;
                new_down = -1;

            } else {

                new_up = 0;
                new_down = 0;

                for (DownloadManager dm : dms) {

                    DownloadManagerStats stats = dm.getStats();

                    new_up += stats.getDataSendRate() + stats.getProtocolSendRate();
                    new_down += stats.getDataReceiveRate() + stats.getProtocolReceiveRate();
                }
            }

            upload_rate = new_up;
            download_rate = new_down;
            last_rate_update = now;
        }
    }

    public int getRunStateCapabilities() {
        return (run_states);
    }

    public boolean hasRunStateCapability(int capability) {
        return ((run_states & capability) != 0);
    }

    public boolean[] getPerformableOperations(int[] ops) {
        boolean[] result = new boolean[ops.length];

        Set<DownloadManager> dms = getTaggedDownloads();

        for (DownloadManager dm : dms) {

            int dm_state = dm.getState();

            for (int i = 0; i < ops.length; i++) {

                if (result[i]) {

                    continue;
                }

                int op = ops[i];

                if ((op & TagFeatureRunState.RSC_START) != 0) {

                    if (dm_state == DownloadManager.STATE_STOPPED || dm_state == DownloadManager.STATE_ERROR) {

                        result[i] = true;
                    }
                }

                if ((op & TagFeatureRunState.RSC_STOP) != 0) {

                    if (dm_state != DownloadManager.STATE_STOPPED && dm_state != DownloadManager.STATE_STOPPING
                            && dm_state != DownloadManager.STATE_ERROR) {

                        result[i] = true;
                    }
                }

                if ((op & TagFeatureRunState.RSC_PAUSE) != 0) {

                    if (dm_state != DownloadManager.STATE_STOPPED && dm_state != DownloadManager.STATE_STOPPING
                            && dm_state != DownloadManager.STATE_ERROR) {

                        if (!dm.isPaused()) {

                            result[i] = true;
                        }
                    }
                }

                if ((op & TagFeatureRunState.RSC_RESUME) != 0) {

                    if (dm.isPaused()) {

                        result[i] = true;
                    }
                }
            }
        }

        return (result);
    }

    public void performOperation(int op) {
        Set<DownloadManager> dms = getTaggedDownloads();

        for (final DownloadManager dm : dms) {

            int dm_state = dm.getState();

            if (op == TagFeatureRunState.RSC_START) {

                if (dm_state == DownloadManager.STATE_STOPPED || dm_state == DownloadManager.STATE_ERROR) {

                    rs_async.dispatch(new AERunnable() {
                        public void runSupport() {
                            dm.setStateQueued();
                        }
                    });
                }
            } else if (op == TagFeatureRunState.RSC_STOP) {

                if (dm_state != DownloadManager.STATE_STOPPED && dm_state != DownloadManager.STATE_STOPPING
                        && dm_state != DownloadManager.STATE_ERROR) {

                    rs_async.dispatch(new AERunnable() {
                        public void runSupport() {
                            dm.stopIt(DownloadManager.STATE_STOPPED, false, false);
                        }
                    });
                }
            } else if (op == TagFeatureRunState.RSC_PAUSE) {

                if (dm_state != DownloadManager.STATE_STOPPED && dm_state != DownloadManager.STATE_STOPPING
                        && dm_state != DownloadManager.STATE_ERROR) {

                    rs_async.dispatch(new AERunnable() {
                        public void runSupport() {
                            dm.pause();
                        }
                    });
                }
            } else if (op == TagFeatureRunState.RSC_RESUME) {

                if (dm.isPaused()) {

                    rs_async.dispatch(new AERunnable() {
                        public void runSupport() {
                            dm.resume();
                        }
                    });
                }
            }
        }
    }

    protected void setSupportsTagTranscode(boolean sup) {
        supports_xcode = sup;
    }

    public boolean supportsTagTranscode() {
        return (supports_xcode);
    }

    public String[] getTagTranscodeTarget() {
        String temp = readStringAttribute(AT_XCODE_TARGET, null);

        if (temp == null) {

            return (null);
        }

        String[] bits = temp.split("\n");

        if (bits.length != 2) {

            return (null);
        }

        return (bits);
    }

    public void setTagTranscodeTarget(String uid, String name) {
        writeStringAttribute(AT_XCODE_TARGET, uid == null ? null : (uid + "\n" + name));

        getTagType().fireChanged(this);

        getManager().featureChanged(this, TagFeature.TF_XCODE);
    }

    protected void setSupportsFileLocation(boolean sup) {
        supports_file_location = sup;
    }

    @Override
    public boolean supportsTagInitialSaveFolder() {
        return (supports_file_location);
    }

    @Override
    public boolean supportsTagMoveOnComplete() {
        return (supports_file_location);
    }

    @Override
    public boolean supportsTagCopyOnComplete() {
        return (supports_file_location);
    }

    @Override
    public TagProperty[] getSupportedProperties() {
        return (getTagType().isTagTypeAuto() ? new TagProperty[0] : tag_properties);
    }

    @Override
    public boolean isTagAuto() {
        TagProperty[] props = getSupportedProperties();

        for (TagProperty prop : props) {

            String name = prop.getName(false);

            if (name.equals(TagFeatureProperties.PR_TRACKER_TEMPLATES)) {

                continue;
            }

            int type = prop.getType();

            if (type == TagFeatureProperties.PT_BOOLEAN) {

                Boolean b = prop.getBoolean();

                if (b != null && b) {

                    return (true);
                }
            } else if (type == TagFeatureProperties.PT_STRING_LIST) {

                String[] val = prop.getStringList();

                if (val != null && val.length > 0) {

                    return (true);
                }
            }
        }

        return (false);
    }
}
